// Sources/SwiftProtobufPluginLibrary/CodePrinter.swift - Code output
//
// Copyright (c) 2014 - 2016 Apple Inc. and the project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See LICENSE.txt for license information:
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
//
// -----------------------------------------------------------------------------
///
/// This provides some basic indentation management for emitting structured
/// source code text.
///
// -----------------------------------------------------------------------------

/// Prints code with automatic indentation based on calls to `indent` and
/// `outdent`.
public struct CodePrinter {

    /// Reserve an initial buffer of 64KB scalars to eliminate some reallocations
    /// in smaller files.
    private static let initialBufferSize = 65536

    private static let kNewline: String.UnicodeScalarView.Element = "\n"

    /// The string content that was printed.
    public var content: String {
        String(contentScalars)
    }

    /// See if anything was printed.
    public var isEmpty: Bool { contentScalars.isEmpty }

    /// The Unicode scalar buffer used to build up the printed contents.
    private var contentScalars = String.UnicodeScalarView()

    /// The `UnicodeScalarView` representing a single indentation step.
    private let singleIndent: String.UnicodeScalarView

    /// The current indentation level (a collection of spaces).
    private var indentation = String.UnicodeScalarView()

    /// Keeps track of whether the printer is currently sitting at the beginning
    /// of a line.
    private var atLineStart = true

    /// Keeps track of if a newline should be added after each string to the
    /// print apis.
    private let newlines: Bool

    public init(indent: String.UnicodeScalarView = "  ".unicodeScalars) {
        contentScalars.reserveCapacity(CodePrinter.initialBufferSize)
        singleIndent = indent
        newlines = false
    }

    /// Initialize the printer for use.
    ///
    /// - Parameters:
    ///   - indent: A string (usually spaces) to use for the indentation amount.
    ///   - newlines: A boolean indicating if every `print` and `printIndented`
    ///       should automatically add newlines to the end of the strings.
    public init(
        indent: String.UnicodeScalarView = "  ".unicodeScalars,
        addNewlines newlines: Bool
    ) {
        contentScalars.reserveCapacity(CodePrinter.initialBufferSize)
        singleIndent = indent
        self.newlines = newlines
    }

    /// Initialize a new printer using the existing state from another printer.
    ///
    /// This can be useful to use with generation subtasks, so see if they
    /// actually generate something (via `isEmpty`) to then optionally add it
    /// back into the parent with whatever surounding content.
    ///
    /// This is most useful to then use `append` to add the new content.
    ///
    /// - Parameter parent: The other printer to copy the configuration/state
    ///     from.
    public init(_ parent: CodePrinter) {
        self.init(parent, addNewlines: parent.newlines)
    }

    /// Initialize a new printer using the existing state from another printer
    /// but with support to control the behavior of `addNewlines`.
    ///
    /// This can be useful to use with generation subtasks, so see if they
    /// actually generate something (via `isEmpty`) to then optionally add it
    /// back into the parent with whatever surounding content.
    ///
    /// This is most useful to then use `append` to add the new content.
    ///
    /// - Parameters:
    ///   - parent: The other printer to copy the configuration/state
    ///       from.
    ///   - newlines: A boolean indicating if every `print` and `printIndented`
    ///       should automatically add newlines to the end of the strings.
    public init(_ parent: CodePrinter, addNewlines newlines: Bool) {
        self.init(indent: parent.singleIndent, addNewlines: newlines)
        indentation = parent.indentation
    }

    /// Writes the given strings to the printer, adding a newline after each
    /// string.
    ///
    /// Newlines within the strings are honored and indentention is applied.
    ///
    /// The `addNewlines` value from initializing the printer controls if
    /// newlines are appended after each string.
    ///
    /// If called with no strings, a blank line is added to the printer
    /// (even is `addNewlines` was false at initialization of the printer.
    ///
    /// - Parameter text: A variable-length list of strings to be printed.
    public mutating func print(_ text: String...) {
        if text.isEmpty {
            contentScalars.append(CodePrinter.kNewline)
            atLineStart = true
        } else {
            for t in text {
                printInternal(t.unicodeScalars, addNewline: newlines)
            }
        }
    }

    /// Writes the given strings to the printer, optionally adding a newline
    /// after each string. If called with no strings, a blank line is added to
    /// the printer.
    ///
    /// Newlines within the strings are honored and indentention is applied.
    ///
    /// - Parameters
    ///   - text: A variable-length list of strings to be printed.
    ///   - newlines: Boolean to control adding newlines after each string. This
    ///       is an explicit override of the `addNewlines` value using to
    ///       initialize this `CodePrinter`.
    public mutating func print(_ text: String..., newlines: Bool) {
        if text.isEmpty {
            assert(
                newlines,
                "Disabling newlines with no strings doesn't make sense."
            )
            contentScalars.append(CodePrinter.kNewline)
            atLineStart = true
        } else {
            for t in text {
                printInternal(t.unicodeScalars, addNewline: newlines)
            }
        }
    }

    /// Indents, writes the given strings to the printer, and then outdents.
    ///
    /// Newlines within the strings are honored and indentention is applied.
    ///
    /// The `addNewlines` value from initializing the printer controls if
    /// newlines are appended after each string.
    ///
    /// - Parameter text: A variable-length list of strings to be printed.
    public mutating func printIndented(_ text: String...) {
        indent()
        for t in text {
            printInternal(t.unicodeScalars, addNewline: newlines)
        }
        outdent()
    }

    private mutating func printInternal(
        _ scalars: String.UnicodeScalarView,
        addNewline: Bool
    ) {
        for scalar in scalars {
            // Indent at the start of a new line, unless it's a blank line.
            if atLineStart && scalar != CodePrinter.kNewline {
                contentScalars.append(contentsOf: indentation)
            }
            contentScalars.append(scalar)
            atLineStart = (scalar == CodePrinter.kNewline)
        }
        if addNewline {
            contentScalars.append(CodePrinter.kNewline)
            atLineStart = true
        }
    }

    /// Appended the content of another `CodePrinter`to this one.
    ///
    /// - Parameters:
    ///   - printer: The other `CodePrinter` to copy from.
    ///   - indenting: Boolean, if the text being appended should be reindented
    ///       to the current state of this printer. If the `printer` was
    ///       initialized off of this printer, there isn't a need to reindent.
    public mutating func append(_ printer: CodePrinter, indenting: Bool = false) {
        if indenting {
            printInternal(printer.contentScalars, addNewline: false)
        } else {
            contentScalars.append(contentsOf: printer.contentScalars)
            atLineStart = printer.atLineStart
        }
    }

    /// Increases the printer's indentation level.
    public mutating func indent() {
        indentation.append(contentsOf: singleIndent)
    }

    /// Decreases the printer's indentation level.
    ///
    /// - Precondition: The printer must not have an indentation level.
    public mutating func outdent() {
        let indentCount = singleIndent.count
        precondition(indentation.count >= indentCount, "Cannot outdent past the left margin")
        indentation.removeLast(indentCount)
    }

    /// Indents, calls `body` to do other work relaying along the printer, and
    /// the outdents after wards.
    ///
    /// - Parameter body: A closure that is invoked after the indent is
    ///     increasted.
    public mutating func withIndentation(body: (_ p: inout CodePrinter) -> Void) {
        indent()
        body(&self)
        outdent()
    }
}
